package org.poptest.junit4;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.ParentRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.TestClass;
import org.poptest.junit4.annotation.DelegateTo;
import org.poptest.reflect.ReflectionUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @author Alan Roche
 *         Date: 23/01/11
 *         Time: 20:56
 */
@SuppressWarnings("unchecked")
public class DelegatingRunner extends BlockJUnit4ClassRunner {
    protected final Class<?> testClass;
    private final BlockJUnit4ClassRunner targetRunner;
    private static final String DELEGATE_TYPE_ACCESSOR = "value";

    /**
     * @param testClass
     */
    public DelegatingRunner(final Class<?> testClass) throws InitializationError {
        this(testClass, DelegateTo.class, DELEGATE_TYPE_ACCESSOR);
    }

    /**
     * @param testClass            Unit test class
     * @param delegateAnnotation   The annotation type for the Delegate Target. Can be overrideen in Subclasses.
     * @param delegateTypeAccessor The accessor  (annotation property) on the Delegate Target that will return the target class.. Defaults to value().
     */
    @SuppressWarnings("unchecked")
    public DelegatingRunner(final Class<?> testClass, final Class<? extends Annotation> delegateAnnotation, final String delegateTypeAccessor)
            throws InitializationError {
        super(testClass);
        try {
            this.testClass = testClass;
            // Get the DelegatTo annotation
            final Annotation delegateTarget = testClass.getAnnotation(delegateAnnotation);

            Constructor<? extends BlockJUnit4ClassRunner> constructor = null;

            final Class<? extends BlockJUnit4ClassRunner> targetRunnerClass;
            // Invoke the getValue method on the annotation to find the delegate Runner
            if (delegateTarget != null) {
                targetRunnerClass =
                        (Class<? extends BlockJUnit4ClassRunner>) delegateAnnotation.getMethod(delegateTypeAccessor).invoke(delegateTarget);

            } else {
                targetRunnerClass = BlockJUnit4ClassRunner.class;
            }
            targetRunner = enhanceRunner(targetRunnerClass);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);

        } catch (InvocationTargetException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private BlockJUnit4ClassRunner enhanceRunner(final Class<? extends BlockJUnit4ClassRunner> targetRunnerClass) {
        final Enhancer enhancer = new Enhancer();


//        try {
//            testInstance = testClass.newInstance();
//        } catch (Exception e) {
//            throw new RuntimeException(e);
//        }


        enhancer.setSuperclass(targetRunnerClass);
        enhancer.setCallback(new Interceptor());

        final BlockJUnit4ClassRunner runner = (BlockJUnit4ClassRunner) enhancer.create(new Class[]{Class.class}, new Object[]{testClass});

        // Need to invike the getTestClass method via reflection, is it is protected in junit4 4.6 and earlier
        // :(

        replaceFrameworkMethods(ReflectionUtils.invoke(runner, ParentRunner.class, "getTestClass", TestClass.class));
        return runner;
    }

    final class Interceptor implements MethodInterceptor {
        public Object intercept(Object targetInstance, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            final Object returnValue;
            if (method.getName().equalsIgnoreCase("getTestClass")) {
                returnValue = getTestClass();
            } else if (method.getName().equalsIgnoreCase("validatePublicVoidNoArgMethods")) {
                DelegatingRunner.this.validatePublicVoidNoArgMethods((Class<? extends Annotation>) args[0],
                        (Boolean) args[1], (List<Throwable>) args[2]);
                returnValue = null;
            } else {
                // Just forward the call
                returnValue = methodProxy.invokeSuper(targetInstance, args);
            }
            return returnValue;
        }
    }


    protected void collectInitializationErrors(List<Throwable> errors) {
        final TestClass testClass = getTestClass();
        replaceFrameworkMethods(testClass);
        validatePublicVoidNoArgMethods(BeforeClass.class, true, errors);
        validatePublicVoidNoArgMethods(AfterClass.class, true, errors);
    }

    @SuppressWarnings("unchecked")
    private void replaceFrameworkMethods(final TestClass testClass) {
        try {
            // From TestClasss
            // private Map<Class<?>, List<FrameworkField>> fFieldsForAnnotations= new HashMap<Class<?>, List<FrameworkField>>();

            Field frameworkMethodsField = TestClass.class.getDeclaredField("fMethodsForAnnotations");
            boolean accessible = frameworkMethodsField.isAccessible();
            try {
                frameworkMethodsField.setAccessible(true);
                final Map<Class<?>, List<FrameworkMethod>> methodsMap = (Map<Class<?>, List<FrameworkMethod>>)
                        frameworkMethodsField.get(testClass);
                for (final Class<?> key : methodsMap.keySet()) {
                    if (methodsMap.get(key) != null) {
                        final List<FrameworkMethod> frameworkMethods = methodsMap.get(key);

                        final List<FrameworkMethod> parameterizedFrameworkMethods = new ArrayList<FrameworkMethod>();
                        if (key == Before.class) {
                            parameterizedFrameworkMethods.addAll(getFirstBeforeMethods(testClass));
                            parameterizedFrameworkMethods.addAll(frameworkMethods);
                        } else if (key == BeforeClass.class) {
                            parameterizedFrameworkMethods.addAll(getFirstBeforeClassMethods(testClass));
                            parameterizedFrameworkMethods.addAll(frameworkMethods);
                        } else { // Test method
                            for (final FrameworkMethod frameworkMethod : frameworkMethods) {
                                parameterizedFrameworkMethods.add(wrapTestFrameworkMethod(key, frameworkMethod));
                            }
                        }

                        methodsMap.put(key, parameterizedFrameworkMethods);
                    }
                }
                if (methodsMap.get(Before.class) == null) {
                    final List<FrameworkMethod> parameterizedFrameworkMethods = new ArrayList<FrameworkMethod>();
                    parameterizedFrameworkMethods.addAll(getFirstBeforeMethods(testClass));
                    if (!parameterizedFrameworkMethods.isEmpty()) {
                        methodsMap.put(Before.class, parameterizedFrameworkMethods);
                    }
                }
                if (methodsMap.get(BeforeClass.class) == null) {
                    final List<FrameworkMethod> parameterizedFrameworkMethods = new ArrayList<FrameworkMethod>();
                    parameterizedFrameworkMethods.addAll(getFirstBeforeClassMethods(testClass));
                    if (!parameterizedFrameworkMethods.isEmpty()) {
                        methodsMap.put(BeforeClass.class, parameterizedFrameworkMethods);
                    }
                }

            } finally {
                frameworkMethodsField.setAccessible(accessible);
            }
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }


    public List<? extends FrameworkMethod> getFirstBeforeMethods(final TestClass testClass) {
        return new ArrayList<FrameworkMethod>();
    }

    public List<? extends FrameworkMethod> getFirstBeforeClassMethods(final TestClass testClass) {
        return new ArrayList<FrameworkMethod>();
    }


    protected FrameworkMethod wrapTestFrameworkMethod(final Class<?> key, final FrameworkMethod frameworkMethod) {
        return new ParameterizedFrameworkMethod(key, frameworkMethod);
    }


    @Override
    public Description getDescription() {
        return targetRunner.getDescription();
    }

    public Runner getTargetRunner() {
        return targetRunner;
    }

    @Override
    public void run(final RunNotifier runNotifier) {
        getTargetRunner().run(runNotifier);
    }
}
